from __future__ import annotations

"""
Utility helpers for reading metadata from Xcode project.pbxproj files.
"""

import json
import os
import re
import shlex
import subprocess
from dataclasses import dataclass, field
from glob import glob, has_magic
from pathlib import Path
from typing import Dict, Iterable, List, Optional, Set


@dataclass
class XcodeBuildMetadata:
    """Subset of build settings relevant for dependency analysis."""

    header_search_paths: Set[str] = field(default_factory=set)
    bridging_headers: Set[str] = field(default_factory=set)
    prefix_headers: Set[str] = field(default_factory=set)


def _run_plutil_to_json(pbxproj_path: Path) -> Optional[Dict]:
    """
    Convert a project.pbxproj file to JSON using macOS plutil.
    Returns parsed json dict on success, otherwise None.
    """
    try:
        completed = subprocess.run(
            ["plutil", "-convert", "json", "-o", "-", str(pbxproj_path)],
            check=True,
            capture_output=True,
            text=True,
        )
        return json.loads(completed.stdout)
    except (subprocess.CalledProcessError, json.JSONDecodeError):
        return None


def _coerce_to_iterable(value) -> Iterable[str]:
    if value is None:
        return []
    if isinstance(value, list):
        return value
    if isinstance(value, str):
        parts = shlex.split(value, posix=False)
        return parts if parts else [value]
    return [str(value)]


_MACRO_PATTERN = re.compile(r"\$\(([A-Za-z0-9_:$]+)\)")
_ALT_MACRO_PATTERN = re.compile(r"\$\{([A-Za-z0-9_:$]+)\}")


def _sanitize_value(raw: str) -> str:
    value = raw.strip().strip('"').strip("'")
    value = value.replace("\\", "")
    return value


def _expand_macros(raw: str, context: Dict[str, str]) -> str:
    def _replace(match: re.Match) -> str:
        key = match.group(1)
        if ":" in key:
            key, _, _modifier = key.partition(":")
        return context.get(key, "")

    value = _MACRO_PATTERN.sub(_replace, raw)
    value = _ALT_MACRO_PATTERN.sub(_replace, value)
    return value


def _resolve_path(value: str, project_dir: Path, context: Dict[str, str]) -> List[str]:
    sanitized = _sanitize_value(value)
    expanded = _expand_macros(sanitized, context)
    if not expanded or "$(" in expanded or "${" in expanded:
        return []

    expanded = os.path.expanduser(expanded)
    if not os.path.isabs(expanded):
        expanded = str(project_dir.joinpath(expanded).resolve())

    if has_magic(expanded):
        matched = glob(expanded, recursive=True)
        return [str(Path(match).resolve()) for match in matched if os.path.exists(match)]

    return [str(Path(expanded).resolve())]


def _bool_setting(value) -> bool:
    if isinstance(value, bool):
        return value
    if isinstance(value, str):
        return value.strip().upper() in {"YES", "TRUE", "1"}
    return bool(value)


def extract_build_metadata(pbxproj_path: str) -> Optional[XcodeBuildMetadata]:
    """
    Parse project file and extract build settings relevant to dependency analysis.
    """
    pbx_path = Path(pbxproj_path)
    project_dir = pbx_path.parent.parent.resolve()
    data = _run_plutil_to_json(pbx_path)
    if not data:
        return None

    objects: Dict[str, Dict] = data.get("objects", {})
    metadata = XcodeBuildMetadata()

    root_object = objects.get(data.get("rootObject"), {})
    project_name = root_object.get("name") or project_dir.stem

    base_context = {
        "SRCROOT": str(project_dir),
        "SOURCE_ROOT": str(project_dir),
        "PROJECT_DIR": str(project_dir),
        "PROJECT_NAME": project_name,
    }

    def process_build_settings(settings: Dict, extra_context: Optional[Dict[str, str]] = None):
        if not isinstance(settings, dict):
            return

        context = dict(base_context)
        if extra_context:
            context.update(
                {key: value for key, value in extra_context.items() if isinstance(value, str)}
            )
        # allow build settings to provide module names for macro replacement
        for key in ["PRODUCT_MODULE_NAME", "PRODUCT_NAME", "SWIFT_MODULE_NAME", "MODULE_NAME"]:
            if key in settings and isinstance(settings[key], str):
                context[key] = settings[key]

        for header_setting in ("HEADER_SEARCH_PATHS", "USER_HEADER_SEARCH_PATHS"):
            for value in _coerce_to_iterable(settings.get(header_setting)):
                for resolved in _resolve_path(value, project_dir, context):
                    metadata.header_search_paths.add(resolved)

        bridging = settings.get("SWIFT_OBJC_BRIDGING_HEADER")
        if bridging:
            for value in _coerce_to_iterable(bridging):
                for resolved in _resolve_path(value, project_dir, context):
                    metadata.bridging_headers.add(resolved)

        prefix_header = settings.get("GCC_PREFIX_HEADER")
        if prefix_header and _bool_setting(settings.get("GCC_PRECOMPILE_PREFIX_HEADER", True)):
            for value in _coerce_to_iterable(prefix_header):
                for resolved in _resolve_path(value, project_dir, context):
                    metadata.prefix_headers.add(resolved)

    def process_configuration_list(config_list_id: str, extra_context: Optional[Dict[str, str]] = None):
        config_list = objects.get(config_list_id)
        if not config_list:
            return

        for config_id in config_list.get("buildConfigurations", []):
            build_config = objects.get(config_id)
            if not build_config:
                continue
            settings = build_config.get("buildSettings", {})
            process_build_settings(settings, extra_context)

    # Project level configurations
    project_config_list_id = root_object.get("buildConfigurationList")
    if project_config_list_id:
        process_configuration_list(project_config_list_id, extra_context={"TARGET_NAME": project_name})

    # Target level configurations
    for obj in objects.values():
        if obj.get("isa") != "PBXNativeTarget":
            continue
        target_name = obj.get("name") or obj.get("productName")
        extra_context = {
            "TARGET_NAME": target_name or "",
            "PRODUCT_NAME": obj.get("productName", target_name or ""),
        }
        config_list_id = obj.get("buildConfigurationList")
        if config_list_id:
            process_configuration_list(config_list_id, extra_context=extra_context)

    return metadata
